package nl.ViewerServer.ServerManager.Paraview;

import java.net.InetAddress;
import java.util.ArrayList;

import javax.xml.rpc.ServiceException;

import nl.ViewerServer.Application;
import nl.ViewerServer.Exceptions.ParameterException;
import nl.ViewerServer.Exceptions.ParaviewConnectionException;
import nl.ViewerServer.Exceptions.PythonException;
import nl.ViewerServer.ServerManager.DataManager.Objects.JavaPythonUtil;
import nl.ViewerServer.ServerManager.DataManager.Objects.Parameter;
import nl.ViewerServer.ServerManager.JobManager.ParaViewThreadAssignment;

import org.apache.axis.AxisFault;
import org.apache.axis.client.Call;
import org.apache.axis.client.Service;
import org.apache.log4j.Logger;

/**
 * An interface to all paraview functionality. This class can be used for only one paraview connection; after closing a
 * new ParaviewInterface object should be instantiated
 * 
 * 
 * @author Administrator
 * 
 */

public class ParaviewInterface {
   private static Logger            log                  = Logger.getLogger( ParaviewInterface.class );

   private String                   endpoint             = null;
   private Process                  paraviewProcess      = null;
   protected boolean                connected            = false;
   protected boolean                inputStreamsShutdown = false;
   protected int                    workerId;
   private ParaViewThreadAssignment threadAssignment     = null;
   private Service                  service;
   private Call                     call;
   private InputStreamReaderThread  inputStreamThread    = null;
   private InputStreamReaderThread  errorStreamThread    = null;

   private boolean                  shuttingDown         = false;

   public ParaviewInterface( int workerId, ParaViewThreadAssignment processCount ) throws ParaviewConnectionException {
      this.workerId = workerId;
      this.threadAssignment = processCount;

      service = new Service();
      connect();
   }

   protected void finalize() throws Throwable {
      try {
         // last effort attempt
         shutdown();
      }
      finally {
         super.finalize();
      }
   }

   public void inputStreamShutdown() {
      inputStreamsShutdown = true;

      shutdown();
   }

   private void forceShutdown() {
      if( connected && paraviewProcess != null ) {
         try {
            paraviewProcess.exitValue();
            log.debug( "Paraview process terminated normally" );
         }
         catch( Exception e ) {
            // still running
            log.info( "Paraview process still running, destroying the process..." );
            paraviewProcess.destroy();
         }
      }
   }

   /**
    * Shutdown method to close the paraview proces connected to a worker
    * 
    * First, try to send a quit command, and check for normal paraview proces termination After a given timeout, destroy
    * the paraview proces completely
    * 
    */

   public void shutdown() {
      if( shuttingDown || connected == false )
         return;

      shuttingDown = true;
      Thread quitThread = new Thread() {
         public void run() {
            log.debug( "Disconnecting paraview process for worker #" + workerId );

            try {
               doQuit();
            }
            catch( Exception e ) {
               // ignore
            }

            if( paraviewProcess != null ) {
               boolean exited = false;

               long start = System.currentTimeMillis();
               log.debug( "ParaviewInterface: waiting for paraview proces to exit" );
               while( !exited && ( System.currentTimeMillis() - start ) < 5000 ) {
                  try {
                     paraviewProcess.exitValue();
                     // if we get here, we are done
                     connected = false;
                     exited = true;
                  }
                  catch( IllegalThreadStateException itse ) {
                     // ignore
                     try {
                        Thread.sleep( 1 );
                     }
                     catch( InterruptedException ie ) {
                        // ignore
                     }
                  }
               }
               log.debug( "ParaviewInterface: waiting for input streams to close" );
               // wait for the input streams for the process to finish
               try {
                  inputStreamThread.join( 2500 );

                  errorStreamThread.join( 2500 );
               }
               catch( Exception e ) {
                  // ignore
               }
            }

         }
      };
      quitThread.start();

      // timeout below is a result of 5 secs waiting for the doQuit command to be handled, and 2x 2500ms
      // for the input/error streams to be closed
      try {
         quitThread.join( 11000 );
      }
      catch( InterruptedException ie ) {
         // ignore
      }
      forceShutdown();
   }

   private boolean connect() throws ParaviewConnectionException {
      if( connected )
         return true;

      if( shuttingDown )
         return false;

      // start the paraview process
      ProcessBuilder pb = new ProcessBuilder();

      try {
         log.info( "MachineFile used: " + threadAssignment.machineFile );
         // log.info( "Python server script used: " + Application.config.pythonServerScript );

         // todo: get host and service information below from the context

         pb.command( "mpiexec", "-recvtimeout", "100", "-machinefile", threadAssignment.machineFile, "-np",
               "" + threadAssignment.totalThreadCount, Application.config.paraviewExecutablePath + "/pvbatch", "--use-offscreen-rendering",
               Application.config.pythonServerScript, "--gwHost=" + InetAddress.getLocalHost().getHostAddress(),
               "--gwPort=8080", "--gwService=/ViewerServer/services/Viewer" );

         String command = "Running command:";

         for( String c : pb.command() ) {
            command += " " + c;
         }

         log.info( command );
         // pb.directory( Application.config.paraviewExecutablePath );

         paraviewProcess = pb.start(); // this will throw an exception when it cannot be started
         if( paraviewProcess != null ) {
            inputStreamThread = new InputStreamReaderThread( paraviewProcess.getInputStream(), "stdout", this );
            errorStreamThread = new InputStreamReaderThread( paraviewProcess.getErrorStream(), "stderr", this );
         }
         long start = System.currentTimeMillis();
         long timeoutTime = start + Application.config.paraviewExecutableStartupTimeout;
         boolean success = false;
         log.debug( "Waiting for python return port" );
         // wait for soap server information
         while( !success && !inputStreamsShutdown && System.currentTimeMillis() < timeoutTime ) {
            if( inputStreamThread != null && inputStreamThread.pythonSoapServer != null ) {
               log.debug( "Python SOAP server waiting at " + inputStreamThread.pythonSoapServer );
               this.endpoint = "http://" + inputStreamThread.pythonSoapServer;
               success = true;
            }
            Thread.sleep( 1 );
         }
         success = false;
         log.debug( "Trying Python SOAP server ping" );
         while( !success && !inputStreamsShutdown && System.currentTimeMillis() < timeoutTime ) {
            // try to ping the process
            try {
               doPing();
               success = true;
            }
            catch( Exception e ) {
               if( e.toString().indexOf( "ConnectException" ) > 0 ) {
                  // ignore
               } else {
                  log.warn( "Unexpected exception while trying to reach Paraview process", e );
               }
            }

            Thread.sleep( 50 );
         }
         if( success ) {
            log.debug( "Connected in " + ( System.currentTimeMillis() - start ) + " ms" );
            connected = true;
            return true;
         }
         log.error( "ParaView connection timed out" );
      }
      catch( Exception e ) {
         log.warn( "Unexpected exception while connecting to Paraview", e );
      }
      shutdown();
      throw new ParaviewConnectionException( "Paraview connection error: Server did not respond to ping" );
   }

   private synchronized Object pythonCall( String method, String[] parameterNames, Object[] parameters, boolean debug )
         throws ParaviewConnectionException, PythonException {
      if( shuttingDown )
         return null;
      try {
         call = (Call)service.createCall();
      }
      catch( ServiceException se ) {
         throw new ParaviewConnectionException( se );
      }

      long start = System.nanoTime();

      Object result = SoapCalls.callMethod( call, endpoint, method, org.apache.axis.Constants.XSD_ANY, parameterNames,
            parameters );

      long end = System.nanoTime();

      if( debug ) {
         log.info( "PythonCall '" + method + "' successful (" + ( (double)( end - start ) / 1000000.0 ) + " ms): " );
         /*
          * if( result != null && result.getClass() == String[].class ) { String[] r = (String[]) result; for( int i =
          * 0; i < r.length; i++ ) { log.info( r[i] ); } } else { log.info( result ); }
          */
      }
      if( result != null && result.getClass() == String.class && ( (String)result ).startsWith( "-" ) ) {
         throw new PythonException( "Method " + method + " failed: " + result );
      }
      return result;

   }

   /**
    * Ask the python soap server to respond
    * 
    * @return
    * @throws ParaviewConnectionException
    * @throws PythonException
    */
   public synchronized Object doPing() throws ParaviewConnectionException, PythonException {
      if( shuttingDown )
         return null;
      return pythonCall( "ping", new String[] {}, new Object[] {}, true );
   }

   /**
    * Ask the python soap server to terminate itself
    * 
    * @return
    * @throws ParaviewConnectionException
    * @throws PythonException
    */
   public String doQuit() throws ParaviewConnectionException, PythonException {
      long s = System.nanoTime();
      String result;
      try {
         call = (Call)service.createCall();
         result = (String)SoapCalls.callMethod( call, endpoint, "quit", org.apache.axis.Constants.XSD_STRING,
               new String[] {}, new Object[] {} );
      }
      catch( ServiceException se ) {
         throw new ParaviewConnectionException( "Could not create SOAP call" );
      }
      catch( ParaviewConnectionException a ) {
         if( a.getCause() instanceof AxisFault ) {
            AxisFault ax = (AxisFault)a.getCause();
            // we expect this, don't throw exception
            return "" + ax.getFaultCode();
         } else {
            throw a;
         }
      }
      long e = System.nanoTime();
      long lastCallDuration = ( e - s ) / 1000 / 1000;
      return result + " (" + lastCallDuration + " ms)";
   }

   public synchronized String setupSource( String script, boolean debug ) throws ParaviewConnectionException,
         PythonException {
      if( shuttingDown )
         return null;
      log.info( "setupSource()" );
      if( script != null && script.length() > 0 )
         return (String)pythonCall( "setupSource", new String[] { "sourceScript" }, new Object[] { script }, debug );

      throw new PythonException( "Gateway says: source script not specified" );

   }

   public synchronized String setupAlignedModalitySource( String script, String label, boolean debug )
         throws ParaviewConnectionException, PythonException {
      if( shuttingDown )
         return null;
      log.info( "setupAlignedModalitySource()" );
      if( script != null && script.length() > 0 )
         return (String)pythonCall( "setupAlignedModalitySource", new String[] { "sourceScript", "sourceLabel" },
               new Object[] { script, label }, debug );

      throw new PythonException( "Gateway says: source script not specified" );
   }

   public synchronized String setupPipeline( String script, boolean debug ) throws ParaviewConnectionException,
         PythonException {
      if( shuttingDown )
         return null;
      log.info( "setupPipeline()" );
      if( script != null && script.length() > 0 )
         return (String)pythonCall( "setupPipeline", new String[] { "pipelineScript" }, new Object[] { script }, debug );

      throw new PythonException( "Server says: pipeline script not specified" );
   }

   public synchronized String executePipelineOutput( String script, boolean debug ) throws ParaviewConnectionException,
         PythonException {
      if( shuttingDown )
         return null;
      log.info( "executePipelineOutput()" );

      if( script != null && script.length() > 0 )
         return (String)pythonCall( "runOutput", new String[] { "outputScript" }, new Object[] { script }, true );

      log.warn( "Output script not specified" );
      return null;
   }

   public synchronized String setupSession( long sessionId, boolean debug ) throws ParaviewConnectionException,
         PythonException {
      if( shuttingDown )
         return null;

      log.info( "setupSession()" );
      return (String)pythonCall( "setupSession", new String[] { "sessionId" }, new Object[] { sessionId }, debug );
   }

   // public synchronized Line[] getAnnotationGeometry( int annotationIndex, boolean debug ) throws
   // ParaviewConnectionException,
   // PythonException {
   //      
   // String geometry = (String)pythonCall( "getAnnotationGeometry", new String[] { "annotationIndex" }, new Object[] {
   // annotationIndex }, debug );
   //      
   // if(geometry == null || geometry.length() <= 2) {
   // return new Line[0];
   // }
   // try {
   //         
   // String[] split = geometry.split( "," );
   // Double[] elem = new Double[6];
   // Line[] lines = new Line[split.length/6];
   //         
   // for(int i = 0; i < split.length; i++) {
   // if(i == 0) {
   // elem[i%6] = new Double( split[0].substring(1) );
   // } else if(i == split.length-1) {
   // elem[i%6] = new Double( split[i].substring(0, split[i].length()-1) );
   // } else {
   // elem[i%6] = new Double( split[i] );
   // }
   //            
   // if(i % 6 == 5) {
   // lines[i/6] = new Line(elem);
   // }
   // }
   //         
   // if(split.length % 6 != 0) {
   // log.warn( "getAnnotationGeometry should return tuples of size 6" );
   // }
   //         
   // return lines;
   // }
   // catch(NumberFormatException nfe) {
   // throw new PythonException("Could not parse doubles in result of getAnnotationGeometry: "+nfe);
   // }
   // } public synchronized String[] getParameters( String[] parameterGetters, boolean debug )
   // throws ParaviewConnectionException, PythonException {
   // if( parameterGetters.length == 0 )
   // return new String[ 0 ];
   //
   // return (String[])pythonCall( "getParameters", new String[] { "parameterGetters" },
   // new Object[] { parameterGetters }, debug );
   // }

   public synchronized String setParameters( ArrayList<Parameter> parameters, boolean debug )
         throws ParaviewConnectionException, PythonException {
      log.info( "setParameters" );

      if( parameters.size() == 0 )
         return null;

      int parameterCount = parameters.size();
      String[] parameterSetters = new String[ parameterCount ];
      String[] setterTypes = new String[ parameterCount ];
      String[] parameterValues = new String[ parameterCount ];

      int i = 0;
      for( Parameter p : parameters ) {
         parameterSetters[i] = p.getParameterSetter();
         setterTypes[i] = p.getSetterType().name();
         parameterValues[i] = p.getPythonRepresentation();
         i++;
      }

      return (String)pythonCall( "setParameters",
            new String[] { "parameterSetters", "setterTypes", "parameterValues" },
            new Object[] { parameterSetters, setterTypes, parameterValues }, debug );
   }

   public synchronized String[] updateParameters( ArrayList<Parameter> parameters, boolean debug )
         throws ParaviewConnectionException, PythonException {

      if( parameters.size() == 0 )
         return null;

      String[] parameterGetters = new String[ parameters.size() ];

      int i = 0;
      for( Parameter p : parameters ) {
         parameterGetters[i] = p.getParameterGetter();
         i++;
      }

      String[] pValues = (String[])pythonCall( "getParameters", new String[] { "parameterGetters" },
            new Object[] { parameterGetters }, debug );

      if( pValues == null || pValues.length != parameters.size() )
         throw new PythonException( "getParameters call returned incorrect amount of parameter values from Python" );

      i = 0;
      for( Parameter p : parameters ) {
         try {
            // "?????" is a reserved result, meaning that the parameter could not be fetched (for example when the
            // visualization pipeline does
            // not support some default parameters)
            if( pValues[i].equals( "?????" ) == false ) {
               Object javaObject = JavaPythonUtil.parsePythonObjectToJavaObject( pValues[i] );
               Object javaParameterObject = JavaPythonUtil.convertJavaObjectToParameter( javaObject,
                     p.getParameterType(), p.getParameterLength() );

               p.setParameterValue( javaParameterObject );
            }
         }
         catch( ParameterException pe ) {
            log.warn( "SetParameterValue failed: " + pe + " for parameter " + p.parameterName );
         }
         i++;
      }
      return pValues;
   }

   public boolean isShuttingDown() {
      return shuttingDown;
   }

}
